<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>打开控制台查看结果</div>

    <script>
        console.log('=========Proxy=========');
        // let proxy = new Proxy(target,handler)
        /* Proxy对象的所有用法，都是上面这种形式，不同的只是handler参数的写法。
           new Proxy()用来生成一个Proxy实例，
           target参数表示要拦截的目标对象，
           handler参数是一个对象，用来定制拦截行为。
        */

        /* 案例：拦截读取属性行为 */
       let proxy2 = new Proxy({},{
        get: function(target,propKey){
            return 10;
        }
       })
       proxy2.time = 20;
       console.log(proxy2.time);  // 10
       console.log(proxy2.name);  // 10
       /* 上面代码中，作为构造函数，Proxy接受两个参数
          1、：要代理的目标对象（在本例中是一个空对象）
          2、：配置对象，对于每一个被代理的操作，需要提供一个对象的处理函数，该处理函数将拦截对应的操作。
          上面代码中，配置对象有一个get方法，用来拦截对目标对象属性的访问请求。
          get方法的两个参数分别是目标对象和所要访问的属性。
          这里的拦截函数返回 10 ，所以访问任何属性都得到 10
          也就是说，给目标对象添加了属性值，但是在访问这一层被拦截了，任何访问都会返回拦截的这一层
       */

       // 注意：要使Proxy生效，就必须对Proxy实例（也就是上面的proxy2对象)进行操作，而不是针对目标对象（第一个参数的空对象）进行操作。

       // 如果handler（第二个参数）没有设置任何拦截，就等同于直接通向原对象。
        /* 案例：拦截读取属性行为 */
        let target = {}
        let handler = {}
        let proxy3 = new Proxy(target,handler)
       proxy3.time = 20;
       console.log(proxy3.time);  // 20
       //handler没有设置任何拦截效果，访问proxy就等于访问target

       /* 对象内是可以设置函数的，可以将Proxy对象设置到object.proxy属性，这样就可以在object对象上调用 */
       let object = { proxy : new Proxy(target,handler) }

       /* Proxy实例也可以作为其它对象的原型对象 */
       let proxy4 = new Proxy({},{
        get : function(target,propKsy){
            return 10
        }
       })
       let obj = Object.create(proxy4)
       console.log('====-=-=-=');
       console.log(obj.time = 20);  // {time: 10}    Prototype:Proxy
       console.log(obj);
       /* 上面这段代码中，proxy4对象是obj对象的原型，obj对象本身并没有time属性，所以根据原型链，会在proxy4对象上读取该对象，导致被拦截 */

       /* 同一个拦截器函数，可以设置拦截多个操作 */
       let hand = {
        get : function(target,name){
            if(name === 'prototype'){
                return Object.prototype
            }
            return 'hello,'+name;
        },
        apply:function(target,thisBinding,args){
            return args[0]
        },
        construct:function(target,args){
            return {value:args[1]}
        }
       }
       let fproxy = new Proxy(function(x,y){
        return x + y
       },hand)

       console.log(fproxy(1,2));  // 1
       console.log(new fproxy(1,2));  // {value:2}
       console.log(fproxy.prototype === Object.prototype); // true
       console.log(fproxy.foo = 'Hello,foo');  // Hello,foo

       /* Proxy支持的拦截操作，一共13种 */
       /* 
        1、 get(target, propKey, receiver)：拦截对象属性的读取，比如proxy.foo和proxy['foo']。

        2、set(target, propKey, value, receiver)：拦截对象属性的设置，比如proxy.foo = v或proxy['foo'] = v，返回一个布尔值。

        3、has(target, propKey)：拦截propKey in proxy的操作，返回一个布尔值。

        4、deleteProperty(target, propKey)：拦截delete proxy[propKey]的操作，返回一个布尔值。
        
        5、ownKeys(target)：拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环，返回一个数组。该方法返回目标对象所有自身的属性的属性名，而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
       
        6、getOwnPropertyDescriptor(target, propKey)：拦截Object.getOwnPropertyDescriptor(proxy, propKey)，返回属性的描述对象。
        
        7、defineProperty(target, propKey, propDesc)：拦截Object.defineProperty(proxy, propKey, propDesc）、Object.defineProperties(proxy, propDescs)，返回一个布尔值。
       
        8、preventExtensions(target)：拦截Object.preventExtensions(proxy)，返回一个布尔值。
       
        9、getPrototypeOf(target)：拦截Object.getPrototypeOf(proxy)，返回一个对象。
       
        10、isExtensible(target)：拦截Object.isExtensible(proxy)，返回一个布尔值。
       
        11、setPrototypeOf(target, proto)：拦截Object.setPrototypeOf(proxy, proto)，返回一个布尔值。如果目标对象是函数，那么还有两种额外操作可以拦截。
       
        12、apply(target, object, args)：拦截 Proxy 实例作为函数调用的操作，比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
       
        13、construct(target, args)：拦截 Proxy 实例作为构造函数调用的操作，比如new proxy(...args)。
       */

       console.log('==========Proxy实例的方法==========');
       console.log('=========get()=========');
       /* 此处为上面拦截方法的详细介绍 */
       /* get()
          用于拦截某个属性的读取操作，可以接受三个参数：
          1、目标对象
          2、属性名
          3、Proxy实例本身（操作指向的对象），该参数为可选参数。
       */
      let person = {
        name : '张三'
      }
      let proxy = new Proxy(person,{
        get:function(target,propKey){
            if(propKey in target){
                return target[propKey]
            } else {
                throw new ReferenceError("属性名：" + propKey + "不存在")
            }
        }
      })
      console.log(proxy.name);  // 张三
      // console.log(proxy.age);  // index.html:135 Uncaught ReferenceError: 属性名：age不存在
    /* 上面代码表示，如果访问目标对象不存在的属性，会抛出错误。如果没有这个拦截函数，访问不存在的属性会返回undefined */
    
    /* 
      可以这么理解,Proxy就是一个代理，跟烟酒代理是一样的。
      比如茅台酒代理，有了这个代理，我们就不能直接去茅台公司拿酒，必须通过代理获得。
      代理说多少钱就是多少钱，说没有就是没有。
      Proxy代理的是一个对象，对象被代理后，我们就不能直接访问该对象，只能通过代理访问。
      如果想要获取对象内的某个值，代理说有就是有，说没有就是没有，代理想给你返回什么值就返回什么值。
    */
   // 创建一个对象，用以被Proxy代理
   let figure = {
    name:'东方不败'
   }
   // 创建代理的方法，当通过代理访问目标对象时，此对象中的对应方法会执行
   let handlers = {
    get(target,prop){
        return '我是代理，我说返回什么就是什么'
    }
   }
   // 创建代理，指定代理对象和代理对象操作对象
   let proxys = new Proxy(figure,handlers)
   console.log(proxys.name);  // 我是代理，我说返回什么就是什么
   console.log(proxys.age);  // 我是代理，我说返回什么就是什么

   /* 为什么要有代理：
      与以前的Object.defineProperty方法一样，就是不希望用户直接访问某个对象，直接操作对象的某个成员，通过代理，用户的访问首先被代理拦截，拦截后就可以对数据进行处理，比如验证等操作。
   */

   console.log('=========set()=========');
   /* 
     set()方法用来拦截某个属性的赋值操作，接收四个参数，依次为：
     1、目标对象
     2、属性名
     3、属性值
     4、Proxy实例本身(可选)
   */
  /* 
     案例：属性值不应该大于200的整数
     假设Person对象有一个age属性，该属性应该是一个不大于200的整数，那么可以使用Proxy保证age属性值符合要求。
  */
 let numHold = {
    set: function(obj,prop,value){
        if(prop === 'age'){
            if(!Number(value)){
                throw new TypeError('参数不是数字')
            }
            if(value > 200){
                throw new RangeError('参数不能大于200')
            }
        }

        // 如果条件满足则直接保存,将值赋值给该属性
        obj[prop] = value
    }
 }
 let persons = new Proxy({},numHold)
 persons.age = 100
 console.log(persons.age);  // 100
 // persons.age = 300  // 报错，参数不能大于200
 // persons.age = '东方'  // 报错，参数不是数字
 /* 上面代码中，设置了一个存值函数set，对对象的age属性进行赋值时，如果不满足age属性的赋值要求，都会抛出一个对应错误，这里就是数据验证的一种实现方法。
     利用set方法，还可以进行数据绑定，也就是每当对象发生变化时，自动更新DOM。
 */


 /* 有时候，我们会在对象上面设置内部属性，属性名的第一个字符使用下划线开头，表示这些属性不应该被外部使用，结合get和set方法，就可以做到防止这些内部属性被外部读写。 */
 // 拦截方法
 let handler2 = {
    get(target,key){
        invariant(key,'get')
        return target[key]
    },
    set(target,key,value){
        invariant(key,'set');
        target[key] = value
        return true
        
    }
 }
 function invariant(key,action){
    if(key[0] === '_'){
        throw new Error(`${action}私有属性${key}无效`)
    }
 }
 let target2 = {}  // 当前对象
 let proxy5 = new Proxy(target2,handler2)
 // console.log(proxy5._prop);  // get私有属性_prop无效
 // proxy5._name = '东方不败' // set私有属性_name无效

 console.log('======apply()======');
 /* apply方法拦截函数的调用，call和apply操作 */
 
 /* apply方法可以接受三个参数，分别是：
    1、目标对象
    2、目标对象的上下文对象（this)
    3、目标对象的参数数组
 */
// let handler3 = {
//     apply(target,ctx,args){
//         return Reflect.apply(...arguments)
//     }
// }

// 下面是一个案例
let target3 = function(){return '东方不败'}
let handler4 = {
    apply:function(){
        return '我是Proxy'
    }
}
let p = new Proxy(target3,handler4)
console.log(p());  // 我是Proxy
/* 上面代码中，变量p为Proxy的实例，当它作为函数调用时，就会被apply方法拦截，返回apply的返回结果 */

/* 下面是另一个案例 */
let twice = {
    apply(target,ctx,args){
        // console.log(Reflect.apply(...arguments)); // 3
        return Reflect.apply(...arguments) * 2
    }
}
function sum(left,right){
    return left + right  // 1 + 2
}
let proxy6 = new Proxy(sum,twice)
console.log(proxy6(1,2)); // 6
/* 上面代码中，每当执行Proxy函数（直接调用或call和apply调用），都会被apply方法拦截 */
console.log(proxy6.call(null,2,3)); // 10

console.log('========has()========');
/* 
   has()方法用来拦截HasProperty操作，即判断对象是否具有某个属性时，该方法会生效。典型的操作就是in运算符。
   has()方法接受两个参数：
   1、目标对象
   2、需要查询的属性名
*/
// in运算符：in操作符用来判断某个属性属于某个对象，可以是对象的直接属性，也可以是通过prototype继承的属性。
/* 下面是一个案例，使用has()方法隐藏某些属性，不被in运算符发现 */
let handler5 = {
    has(target,key){
        // key为传入的属性名，这里key[0]就是属性名的第一个字符
        if(key[0] === '_'){
            return false
        }
        return key in target
    }
}
let target5 = { _prop:'foo',prop:'fuu' }
let proxys5 = new Proxy(target5,handler5)
console.log('_prop' in proxys5);  // false  '_prop'属性不属于proxys5对象
console.log('_prop' in target5);  // true  '_prop'属性属于target5对象
// 注意：如果原对象不可配置或禁止扩展，这是has()拦截就会报错
// 注意：虽然for...in循环也用到了了in运算符，但是has()拦截对for...in循环不生效。

console.log('=======construct()======');
/* construct()方法用于拦截new命令，下面是拦截对象的写法 */
const handler7 = {
    construct(target,args,newTarget){
        return new target(...args)
    }
}
/* 
  construct()方法可以接受三个参数
  1、目标对象
  2、构造函数的参数数组
  3、创造实例对象时，new命令作用的构造函数(也就是下面例子中的p2)
*/
let con = {
    construct:function(target,args){
        // target是一个函数(){}
        // args是一个参数数组
        // this : construct
        console.log(this === con); // true
        console.log('回调：'+args.join(','));  // 回调：1,2
        return { value : args[0] * 10 }  // construct返回的必须是一个对象，否则报错
        // return 1  // 返回的不是对象，报错'construct' on proxy: trap returned non-object ('1')
    }
}
let p2 = new Proxy(function(){},con)
console.log((new p2(1,2))); 
// 结果：
// 回调：1
// { value : 10}
/* 注意：由于construct()拦截的是构造函数，所以它的目标对象必须是函数，否则会报错 */
/* 注意：construct()中的this指向的是con，而不是实例对象 */

console.log('=========deleteProperty()=========');
/* deleteProperty方法用于拦截delete操作，如果这个方法抛出错误或返回false，当前属性就无法被delete命令删除 */
let del = {
    deleteProperty(target,key){
        inv(key,'delete')
        delete target[key]  // 如inv未抛出错误就证明是可以删除的，删除操作
        return true  // 抛出true
    }
}
function inv(key,action){
    // 如果属性名的第一个字符是_说明是私有属性，抛出错误
    if(key[0] === '_'){
        throw new Error(`当前操作：${action}对于私有属性${key}无效`)
    }
}
let target7 = {_prop:'foo'}
let proxy7 = new Proxy(target7,del)
// console.log(delete proxy7._prop);  // 报错：当前操作：delete对于私有属性_prop无效
// 注意，目标对象自身的不可配置（configurable）的属性，不能被deleteProperty方法删除，否则报错。

console.log('=========defineProperty ()========');
/* 
   defineProperty()方法拦截Object.defineProperty()操作
*/
let h = {
    defineProperty (target,key,desc){
        // target 目标对象
        // 目标对象的属性名
        // desc目标对象的赋值
        return false
        // return target[key] = desc.value
    }
}
let t = {}
let pr = new Proxy(t,h)
console.log(pr.foo = 'bar');  // 不会生效，被拦截
console.log(t);  // {}
/* 上面代码中，defineProperty()方法内部没有任何操作，只返回false,导致新添加的属性总是无效。
   这里返回的false只是用来提示操作失败，本身并不能阻止添加新属性。
*/

console.log('=====剩下的请看index2.html=====');







    


       



    </script>
</body>
</html>